Skip to content

feat: 카카오 알림톡 / SMS / 이메일 전송 기능 추가#40

Open
MU-Software wants to merge 3 commits intomainfrom
feature/add-notification-app
Open

feat: 카카오 알림톡 / SMS / 이메일 전송 기능 추가#40
MU-Software wants to merge 3 commits intomainfrom
feature/add-notification-app

Conversation

@MU-Software
Copy link
Copy Markdown
Member

주요 변경 사항

  • GMail을 통한 이메일 전송 기능 및 NHN Cloud를 통한 카카오 알림톡 / SMS 전송 기능을 추가합니다.
  • 어드민에서 이메일 / 카카오 알림톡 / SMS 전송 시 사용할 템플릿 관리 기능 및 템플릿을 사용한 전송 기능을 구현합니다.

배경 사항

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

본 PR은 운영진이 어드민에서 이메일(Gmail OAuth2 SMTP) / NHN Cloud SMS / NHN Cloud 카카오 알림톡을 템플릿 기반으로 미리보기 및 발송(History 생성/재시도)할 수 있도록 알림 서브시스템을 추가합니다.

Changes:

  • notification 앱(템플릿/히스토리 모델, 템플릿 렌더링, NHN 동기화) 및 프리뷰 HTML 템플릿 추가
  • Gmail OAuth2 기반 SMTP 백엔드와 NHN Cloud(SMS/알림톡) 외부 API 클라이언트 추가 및 설정 확장
  • 어드민 API(ViewSet/Serializer/Filter/URL) 및 관련 테스트 추가, Google OAuth2 authorize/redirect 플로우에 PKCE(code_verifier) 세션 저장 추가

Reviewed changes

Copilot reviewed 34 out of 37 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
app/notification/test/template_test.py 템플릿 변수 추출/렌더링/프리뷰 렌더링 동작 테스트 추가
app/notification/test/sms_test.py SMS/MMS 렌더링 및 History 전송 상태 전이 테스트 추가
app/notification/test/kakao_sync_test.py NHN Cloud 알림톡 템플릿 동기화 및 로컬 CUD 차단 테스트 추가
app/notification/test/history_send_test.py Email History send 상태 전이 및 Slack 로깅 테스트 추가
app/notification/test/init.py 테스트 패키지 초기화 파일 추가
app/notification/templates/nhn_cloud_sms_preview.html SMS/MMS 프리뷰 HTML 템플릿 추가
app/notification/templates/nhn_cloud_kakao_alimtalk_preview.html 카카오 알림톡 프리뷰 HTML 템플릿 추가
app/notification/templates/email_preview.html 이메일 프리뷰 HTML 템플릿 추가
app/notification/models/nhn_cloud_sms.py NHN Cloud SMS 템플릿/히스토리 모델 및 send 파라미터 빌드 추가
app/notification/models/nhn_cloud_kakao_alimtalk.py 알림톡 템플릿 read-only 매니저, NHN 동기화, 모델/히스토리 추가
app/notification/models/email.py 이메일 템플릿/히스토리 모델 및 send 파라미터 빌드 추가
app/notification/models/base.py 템플릿 렌더링/변수추출, History send 상태/Slack 로깅 공통 베이스 추가
app/notification/models/init.py notification 모델 export 구성
app/notification/migrations/0001_initial.py notification 앱 초기 마이그레이션 추가
app/notification/apps.py notification AppConfig 추가
app/notification/init.py notification 패키지 초기화 파일 추가
app/external_api/google_oauth2/views.py OAuth2 authorize에서 code_verifier 세션 저장 및 redirect에서 복원
app/core/util/google_api.py authorization URL 생성 시 code_verifier 반환하도록 변경
app/core/test/nhn_cloud_sms_test.py NHN Cloud SMS client 입력 검증/엔드포인트 분기/로깅 테스트 추가
app/core/test/models_test.py QuerySet.update/bulk_update의 updated_by 주입/보존 회귀 테스트 추가
app/core/test/email_backends_test.py Gmail OAuth2 백엔드 토큰 캐싱 및 XOAUTH2 인증 테스트 추가
app/core/test/init.py core 테스트 패키지 초기화 파일 추가
app/core/settings.py notification 앱/템플릿 경로, 이메일/NHN Cloud 설정 추가
app/core/openapi/schemas.py HTML 응답 스키마 헬퍼(build_html_responses) 추가
app/core/models.py QuerySet.update updated_by 주입 조건 개선 및 select_related_with_user 추가
app/core/external_apis/smtp_email.py SMTP 이메일 발송 클라이언트 추가
app/core/external_apis/nhn_cloud_sms.py NHN Cloud SMS 발송 클라이언트 추가
app/core/external_apis/nhn_cloud_kakao_alimtalk.py NHN Cloud 알림톡 발송/조회 클라이언트 추가
app/core/external_apis/interface.py 외부 발송 인터페이스 및 SendParameters 타입 추가
app/core/email_backends.py Gmail OAuth2 SMTP EmailBackend 구현 및 토큰 캐싱 추가
app/core/const/tag.py Admin 알림 관련 OpenAPI tag 추가
app/admin_api/views/notification.py 어드민 알림 템플릿/히스토리 CRUD 및 render/history/retry 액션 추가
app/admin_api/urls.py 어드민 알림 라우팅(notification/email
app/admin_api/test/notification_test.py 어드민 알림 API 전반(CRUD/필터/프리뷰/히스토리/재시도) 테스트 추가
app/admin_api/test/conftest.py 어드민 알림 API 테스트용 fixture 및 thread_local 격리 추가
app/admin_api/serializers/notification.py 어드민 알림 serializer(템플릿 변수 노출/프리뷰/히스토리 생성) 추가
app/admin_api/filtersets/notification.py 어드민 알림 템플릿/히스토리 필터 추가

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +80 to +84
context[key] = f"RandomValue-{uuid4().hex[:8]}"
case UnhandledVariableHandling.REMOVE:
context[key] = ""

return json_loads(template.render(Context(context)))
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render() builds a JSON string via Django template substitution and then immediately json_loads(...). If any context value contains quotes/newlines or non-string JSON types (e.g., booleans render as True), the rendered output can become invalid JSON (or allow JSON-structure injection), causing runtime failures or unintended payloads. Consider parsing self.data with json.loads first and then rendering only the string leaf values (or JSON-encoding inserted values) so the final payload remains valid JSON regardless of context content.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +35
url, _ = flow.authorization_url(
prompt=prompt,
access_type=access_type,
include_granted_scopes="true" if include_granted_scopes else "false",
)[0]
)
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flow.authorization_url() returns a (url, state) tuple; the state is currently discarded. For OAuth2, the state parameter should be persisted (e.g., in session) and validated on the redirect callback to prevent CSRF/session fixation issues. Please return the state (or store it) and ensure the redirect handler verifies it before exchanging the code.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
url, code_verifier = create_authorization_url(flow=flow)
request.session["google_oauth2_code_verifier"] = code_verifier
return redirect(url)
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authorization step stores code_verifier in session but does not persist/validate the OAuth2 state value. To properly defend against CSRF, store state from authorization_url() in the session here and validate the incoming state query param in the redirect handler before calling fetch_credentials().

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +143
return SendParameters(
payload=self.context,
send_to=self.send_to,
template_code=self.template_code,
sent_from=self.template.sender_key,
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NHNCloudKakaoAlimTalkNotificationHistory sends payload=self.context without validating required template variables. Unlike Email/SMS (which fail-fast via template.render()), this can silently send requests missing required templateParameter keys, leading to provider-side errors or incomplete messages. Consider validating self.context against self.template.template_variables (and raising ValueError listing missing vars) before building send parameters.

Copilot uses AI. Check for mistakes.
Comment thread app/core/models.py
@@ -19,7 +19,9 @@ def create(self, **kwargs: dict) -> models.Model:
return super().create(**(kwargs | {"created_by": current_user, "updated_by": current_user}))

def update(self, **kwargs: dict) -> typing.Self:
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseAbstractModelQuerySet.update() is annotated as returning typing.Self, but QuerySet.update() returns an int (number of rows updated). This mismatch can confuse callers and type checkers. Update the return annotation to int (and adjust any dependent typing if needed).

Suggested change
def update(self, **kwargs: dict) -> typing.Self:
def update(self, **kwargs: dict) -> int:

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +13
class NHNCloudSMSNotificationTemplate(NotificationTemplateBase):
html_template_name: ClassVar[str] = "nhn_cloud_sms_preview.html"

from_no = models.CharField(max_length=13, null=True, blank=True)

Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_no is nullable/blank, but NHNCloudSMSClient.send_message() raises when sent_from is falsy. As a result, the API currently allows creating SMS templates that can never be sent (will always transition histories to FAILED). Consider making from_no required at the model/serializer level (or adding explicit validation) so misconfigured templates are rejected earlier.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@earthyoung earthyoung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트랑 template 파일들은 하나하나 세부적으로 보진 못했습니다...ㅠㅠ
개발하느라 고생하셨습니다. LGTM입니다!

Comment on lines +87 to +90
with suppress(Exception):
history.send()

return Response(data=self.get_serializer(history).data)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception이 발생하면 로깅을 하거나 노티를 주는 로직도 추가하면 좋을 것 같습니다!

Comment thread app/core/const/tag.py
Comment on lines +15 to +17
ADMIN_NOTI_EMAIL = "Admin > Notification > Email"
ADMIN_NOTI_KAKAO_ALIMTALK = "Admin > Notification > Kakao Alimtalk"
ADMIN_NOTI_SMS = "Admin > Notification > SMS"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거는 Tag로 level을 나타내는 건가요..! 좋은 것 같습니다!

Comment on lines +63 to +72
@pytest.mark.django_db
def test_queryset_bulk_update_does_not_raise_duplicate_column(template, other_user):
# bulk_update는 내부적으로 `update(updated_by_id=Case(...))`를 호출 → 자동 주입과 충돌하면 안 됨
template.title = "bulked"
template.updated_by = other_user
EmailNotificationTemplate.objects.bulk_update([template], fields=["title", "updated_by"])

template.refresh_from_db()
assert template.title == "bulked"
assert template.updated_by_id == other_user.id
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 구체적인 테스트케이스 작성해주신 부분 넘 좋은 것 같습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants